
Ante la perdida de clientes o deserción de clientes en una empresa de Telecomunicaciones, el precio de adquirir un nuevo cliente suele ser más alto que retener al antiguo. Ante esta problemática a partir de la construcción de un modelo análitico busco identificar segmentos de clientes que corren el riesgo de marcharse de la compañía, para así facilitar el poder involucrarlos de manera proactiva con nuevas estrategías comerciales en lugar de simplemente perderlos.
En este Notebook, comienzo con la construcción de un único tablón analítico a partir de las cosechas del mes de diciembre y enero que poseen información de clientes,productos,consumo y financiación. Con la ayuda de un análisis exploratorio de datos (EDA) pretendo encontrar patrones dentro de cada atributo que me permita obtener información relevante así como un mejor conocimiento de dominio. En base a esto pretendo modificar y crear nuevas características a partir de las características existentes para aumentar el rendimiento de mi modelo de machine learning, una vez el conjunto de datos esta completamente depurado construyo un modelo de clasificación que sea capaz de predecir la no permanencia de los clientes en la compañía todo esto con la ayuda de diferentes estrategías como la optimización de hyperparametros, proceso de regularización para evitar overfitting,ensamble de modelos entre otros.
Change Logs
2020/03/25: Fix xgboost of ensemble part. 2000 estimators broke runtime limit, so I reduced it to 800 estimators.
# Basic Libraries
import numpy as np
import pandas as pd
import csv
import seaborn as sns
import seaborn as sns; sns.set()
# avanced Notebook
import pandas as pd
from beakerx import *
# HTML
import ipywidgets as widgets
from IPython.display import display, HTML
import sys
# warnings — Warning control
import warnings
warnings.filterwarnings('ignore')
# Html document analysis (web Scraping)
import requests
from bs4 import BeautifulSoup
import re
# convert to dates
import datetime
from datetime import datetime, timedelta
#Coding categorical labels in numbers
from sklearn.preprocessing import LabelEncoder
# Division dataset Train/test
from sklearn.model_selection import train_test_split
# Feature Scaling
from sklearn.preprocessing import RobustScaler
# collections
import collections
import os
#print(os.listdir("dir"))
# Visaulization
import matplotlib.pyplot as plt
import seaborn as sns
from plotnine import *
#from ggplot import *
#%matplotlib inline
# Análisis VIF
from sklearn.linear_model import LinearRegression
#Continuous variable normalization
from scipy.stats import zscore
#Metrics
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score, auc, confusion_matrix, f1_score, precision_score, recall_score, roc_curve
# Features Selection
from sklearn.feature_selection import VarianceThreshold
from sklearn.feature_selection import RFE
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
from sklearn.feature_selection import RFECV # Recursive feature elimination with cross validation
# Classifier (machine learning algorithm)
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier
from xgboost import XGBClassifier
# Desbalanced Class
from imblearn.under_sampling import NearMiss
from imblearn.over_sampling import RandomOverSampler
from imblearn.combine import SMOTETomek
from imblearn.ensemble import BalancedBaggingClassifier
from sklearn.model_selection import KFold
from collections import Counter
from imblearn.metrics import classification_report_imbalanced
# save models
from sklearn.externals import joblib
# Evaluation
from sklearn.model_selection import cross_val_score, cross_val_predict
from pylab import rcParams
# Parameter Tuning
from sklearn.model_selection import GridSearchCV
# Settings
pd.options.mode.chained_assignment = None # Stop warning when use inplace=True of fillna
clientes_dic = pd.read_csv('../input/cosecha-dic-ene/clientes_dic.csv')
consumo_dic = pd.read_csv('../input/cosecha-dic-ene/consumo_dic.csv')
productos_dic = pd.read_csv('../input/cosecha-dic-ene/productos_dic.csv')
financiacion_dic = pd.read_csv('../input/cosecha-dic-ene/financiacion_dic.csv')
clientes_ene = pd.read_csv('../input/cosecha-dic-ene/clientes_ene.csv')
consumo_ene = pd.read_csv('../input/cosecha-dic-ene/consumo_ene.csv')
productos_ene = pd.read_csv('../input/cosecha-dic-ene/productos_ene.csv')
financiacion_ene= pd.read_csv('../input/cosecha-dic-ene/financiacion_ene.csv')
df_dic = pd.read_csv("df_dic.csv")
df_ene = pd.read_csv("df_ene.csv")
total = len(df_dic)*1.
plt.figure(figsize=(8,6))
ax = sns.countplot(x="_merge", data=df_dic, order=[0,1])
plt.title('Distribution of Target',size=25)
plt.xlabel('Permanencia y No permanencia de Clientes',va="center",size=15.1)
plt.ylabel('Frequency [%]')
for p in ax.patches:
ax.annotate('{:.1f}%'.format(100*p.get_height()/total), (p.get_x()+0.2, p.get_height()+5), va="top", size=20)
ax.yaxis.set_ticks(np.linspace(0, total, 10))
_ = ax.set_yticklabels(map('{:.1f}%'.format, 100*ax.yaxis.get_majorticklocs()/total))
Es un ejmplo de como obtuve la target a partir de la unión de los tablones análiticos de cada mes cosecha diciembre y enero. Lo anterior son los datasets modificados.
df1_dic= clientes_dic.merge(consumo_dic, how='outer', indicator='union')
df2_dic = df1_dic.merge(productos_dic,how='outer', indicator='exists')
df3_dic= df2_dic.merge(financiacion_dic,how='outer', indicator='exists2')
df3_dic=df3_dic.drop(['union', 'exists'], axis=1)
df1_ene = clientes_ene.merge(consumo_ene, how='outer', indicator='union')
df2_ene= df1_ene.merge(productos_ene,how='outer', indicator='exists')
df3_ene = df2_ene.merge(financiacion_ene,how='outer', indicator='exists2')
df3_ene=df3_ene.drop(['union', 'exists'], axis=1)
df_dic_ene = pd.merge(df3_dic, df3_ene, how='outer', on='id', suffixes=('_dic', '_ene'), indicator=True)
df_dic_ene =df_dic_ene .drop(['exists2_ene'], axis=1)
filtro = df_dic_ene['_merge'].isin(["both","left_only"])
df_dic_ene=df_dic_ene[filtro]
df_dic_ene['_merge']= (df_dic_ene['_merge'] == 'left_only') +0
# Splitting
#y = df_dic_ene._merge
#x = df_dic_ene.drop('_merge',axis=1)
#X_train, X_test, y_train, y_test = train_test_split(x,y,test_size=0.2,random_state=86,
#stratify = y)
data_dic = df_dic_ene.loc[:, ~df_dic_ene.columns.str.contains("ene$")]
data_ene = df_dic_ene.loc[:, ~df_dic_ene.columns.str.contains("dic$")]
Primero, echemos un vistazo a cómo se ve el conjunto de datos de diciembre y el conjunto de datos de enero.
df_dic.head()
df_ene.head()
df_dic.info()
A lo largo de este Notebook realizare un proceso de limpieza y transformaciones en ambas cosechas (diciembre y enero) pero únicamente trabajaré con la Cosecha del mes de Diciembre con la cuál pretendo entrenar un modelo y predecir la probabilidad que tienen los clientes de enero en marcharse de la compañía, para así simular un escenario de la vida real donde los datos que se van a predecir vienen después.
A continuación, verifico los detalles sobre cada cosecha . Oculto el resultado de cada bloque de código para ahorrar espacio. Haga clic en la pestaña "Output" en el lado derecho de cada bloque de código para ver estos detalles.
También uso .isnull (). Sum () para verificar los datos faltantes (NaN).
print(df_dic.shape)
df_dic.describe()
df_dic.isnull().sum()
print(df_ene.shape)
df_ene.describe()
df_ene.isnull().sum()
En ambas cosechas se observa la presencia de datos faltantes en su mayoría superior al 80% como por ejemplo con las variables (num_dt,incidencia,financiación,impago y descuentos). Con respecto a las variables descuentos o incidencia tengo conocimiento de que los valores Missing es igual a "No"; es decir el cliente no ha tenido algún tipo de descuento o el cliente "No" ha tenido algún tipo de reclamación respectivamente.
Lo primero que haré es visualizar los datos en busqueda de información valiosa/oculta dentro de cada característica. Por lo tanto en esta sesión decido visualizar los datos existentes sin imputar los datos faltantes para llevar un orden. Aunque la variable antiguedad no tiene datos fatltantes está diseñada en formato fecha por tanto todavía no la procesaré extrayendo el dato mensual por lo que no tendrá visualización.
Como dije anteriormente las variables descuentos y incidencia el valor NaN es igual "No" por tanto procederé a rellenarlas para poder visualizarlas.
Aquí utilizó métodos para crear gráficas basadas en el tipo de cada caaracterística.
# Continuous Data Plot
def cont_plot(df, feature_name, target_name, palettemap, hue_order, feature_scale):
df['Counts'] = "" # A trick to skip using an axis (either x or y) on splitting violinplot
fig, [axis0,axis1] = plt.subplots(1,2,figsize=(10,5))
sns.distplot(df[feature_name], ax=axis0);
sns.violinplot(x=feature_name, y="Counts", hue=target_name, hue_order=hue_order, data=df,
palette=palettemap, split=True, orient='h', ax=axis1)
axis1.set_xticks(feature_scale)
plt.show()
# WARNING: This will leave Counts column in dataset if you continues to use this dataset
# Categorical/Ordinal Data Plot
def cat_plot(df, feature_name, target_name, palettemap):
fig, [axis0,axis1] = plt.subplots(1,2,figsize=(10,5))
df[feature_name].value_counts().plot.pie(autopct='%1.1f%%',ax=axis0)
sns.countplot(x=feature_name, hue=target_name, data=df,
palette=palettemap,ax=axis1)
plt.show()
survival_palette = {0: "black", 1: "orange"} # Color map for visualization
la variable edad tiene una distribución uniforme, para una mejor visualización decido agrupar por rango de edad lo que facilita el análisis descriptivo como una generalización de patrones en los datos.
cont_plot(df_dic, 'edad', '_merge', survival_palette, [1, 0], range(0,85,10))
#ages_labels=pd.cut(x=df_dic['edad_dic'], bins=[18,19,30,45,50,60,85],
#labels=["18-19", "19-30", "30-45","45-50","50-60","60+"]).copy()
#cat_plot(df_dic, 'ages_labels', '_merge', survival_palette)
cont_plot(df_dic, 'facturacion', '_merge', survival_palette, [1, 0], range(0,400,15))
cont_plot(df_dic, 'num_lineas', '_merge', survival_palette, [1, 0], range(1,6,1))
df_dic.incidencia.fillna("NO",inplace=True)
cat_plot(df_dic, 'incidencia', '_merge', survival_palette)
plot = pd.crosstab(index=df_dic['_merge'],
columns=df_dic['incidencia']
).apply(lambda r: r/r.sum() *100,
axis=0).plot(kind='bar', stacked=True)
Claramente los clientes con algún tipo de queja o reclamación están insatisfechos con la compañía y gran parte de ellos prefieren No permanecer en ella.
Como verifiqué anteriormente, num_dt tiene datos faltantes. El método de visualización no puede tratar datos faltantes, por lo que elimino las filas con datos faltantes temporalmente. Verá que no sobrescribí dataset y creé un DataFrame para su visualización (num_dt_nonan).
num_dt_nonan = df_dic[['num_dt','_merge']].copy().dropna(axis=0) # Copy dataframe so method won't leave Counts column in train_set
cont_plot(num_dt_nonan , 'num_dt', '_merge', survival_palette, [1, 0], range(1,5,1))
cat_plot(df_dic, 'num_dt', '_merge', survival_palette)
Esta variable tiene potencial para ser eliminada debido a la cantidad de datos faltantes y no parece haber diferencias que ayuden a encontrar patrones y ha clasificar la no permanencia de los clientes.
cat_plot(df_dic, 'TV','_merge', survival_palette)
Intentando ver las diferencias entre Permanecer y No permanecer en la compañía según el tipo de paquete de tv contratado por el cliente para ver si dicha característica tiene un impacto en la No permanencia. No hay diferencias que discriminen la no permanencia en el tipo de paquete contratado independientemente del número de clientes.
cat_plot(df_dic, 'conexion','_merge', survival_palette)
cat_plot(df_dic, 'vel_conexion','_merge', survival_palette)
cont_plot(df_dic, 'num_llamad_ent', '_merge', survival_palette, [1, 0], range(0,300,63))
cont_plot(df_dic, 'num_llamad_sal', '_merge', survival_palette, [1, 0], range(0,100,25))
cont_plot(df_dic, 'mb_datos', '_merge', survival_palette, [1, 0], range(0,25000,6000))
cont_plot(df_dic, 'seg_llamad_ent', '_merge', survival_palette, [1, 0], range(0,20000,5000))
cont_plot(df_dic, 'seg_llamad_sal', '_merge', survival_palette, [1, 0], range(0,20000,5055))
df_dic.financiacion.fillna("NO",inplace=True)
cat_plot(df_dic, 'financiacion', '_merge', survival_palette)
plot = pd.crosstab(index=df_dic['_merge'],
columns=df_dic['financiacion']
).apply(lambda r: r/r.sum() *100,
axis=0).plot(kind='bar', stacked=True)
Parece que hay diferencias que discriminen la permanencia y no permanencia en cuánto a si el cliente tiene algún préstamo para la adquisición de terminales con la compañía.
df_dic.descuentos.fillna("NO",inplace=True)
cat_plot(df_dic, 'descuentos', '_merge', survival_palette)
plot = pd.crosstab(index=df_dic['_merge'],
columns=df_dic['descuentos']
).apply(lambda r: r/r.sum() *100,
axis=0).plot(kind='bar', stacked=True)
Parece que mientras los clientes tengan algún tipo de descuento serán leales a la compañía.
imp_financ_nonan = df_dic[['imp_financ','_merge']].copy().dropna(axis=0) #Copy dataframe so method won't leave Counts column in train_set
cont_plot(imp_financ_nonan, 'imp_financ', '_merge', survival_palette, [1, 0], range(5,45,8))
Althrough the graph has clear difference here, but lets zoom-in to check.
imp_financ_nonan = df_dic[['imp_financ','_merge']].copy()
imp_financ_nonan ['Counts'] = ""
fig, axis = plt.subplots(1,1,figsize=(10,5))
sns.violinplot(x='imp_financ', y="Counts", hue='_merge', hue_order=[1, 0], data=imp_financ_nonan,
palette=survival_palette, split=True, orient='h', ax=axis)
axis.set_xticks(range(5,45,8))
axis.set_xlim(-15,100)
plt.show()
Parece que los clientes que financiarón algun tipo de terminal y el pago mensual es inferior a 5€ o nada, no se sienten obligados a continuar con la compañía y deciden marcharse. Siembargo a medida que el pago mensual aumenta el compromiso de quedarse también, alcanzando puntos máximos en 10€,20€ y con una reducción abrupta a partir del punto máximo de 35€. Por tanto ante tasas muy bajas de abono o muy altas en los terminales financiados el precio suele ser un factor crítico
Decido Categorizar imp_financ en rangos de :
Al igual que en el aprendizaje automático, en lugar de proporcionar datos sin procesar al modelo, puedo mejorarlo modificando las funciones existentes y / o creando nuevas funciones. Usaré la información de la parte anterior de EDA para ayudarme a decidir cómo hacerlo.
Tanto en la cosecha diciembre como la cosecha de enero
df_dic.describe()
df_ene.describe()
Completo los datos faltantes de la cosecha de enero con fillna("NO") que son aquellas categorías donde NaN corresponde a "NO". Ha excepción de la variable num_dt y imp_financ que tanto en diciembre como en enero supera los datos faltantes con más del 80%, estimar estas variables podría introducir sesgo e invalidar o comprometer los resultados obtenidos distorcionandolos en mayor o menor grado.
gráficamente al eliminar las observaciones con datos Faltantes para poder visualizarlas, la variable num_dt no mostraba algún tipo de comportamiento en los datos que llegasen a agrupar los datos para un posterior modelo de clasificación por ende decido crear una nueva variable en su lugar llamada "info_numdt" (0: no hay líneas en impago,1 : entre 1 y 4 lineas en impago) , en cuánto a la variable imp_financ al eliminar las muestras faltantes gráficamente observe patrones interesantes, por tanto ya que los datos fuerón generados aleatoriamente el riesgo de introducir sesgo al eliminar valores Missings en esta variable es muy reducido, por ende decido tramificarla.
En cuanto a las variables categóricas de la cosecha de enero realizaré una imputación con respecto al mayor número de ocurrencias. (moda)
Los valores Faltantes en descuentos,incidencia y financiación ya fuerón completados en el mes de Diciembre. Por tanto procederé a hacer lo mismo con la cosecha de enero.
# Completando Missings Cosecha Enero
df_ene.descuentos.fillna("NO",inplace=True)
df_ene.incidencia.fillna("NO",inplace=True)
df_ene.financiacion.fillna("NO",inplace=True)
figu, axis1 = plt.subplots(1,1,figsize=(10,5))
sns.boxplot(data = df_dic, x = 'financiacion', y = df_dic['imp_financ'].dropna(),
showfliers = True,palette='bright',ax=axis1)
plt.title('Distribucion de Pago Mensual en función de Terminales Financiados')
plt.xlabel('Número de Líneas')
plt.ylabel('Pago TérminalesFinanciados ')
plt.ticklabel_format(style='plain', axis='y')
df_dic.groupby('financiacion')['imp_financ'].median()
# Proporción de Missings por variable
prop_missings_dic = df_dic.apply(lambda x:x.isnull().mean()).copy()
prop_missings_dic
prop_missings_ene = df_ene.apply(lambda x:x.isnull().mean()).copy()
prop_missings_ene
df_dic.drop(["imp_financ"], axis=1, inplace=True)
df_ene.drop(["imp_financ"], axis=1, inplace=True)
Al tener un alto % de Missings superior al 80% decido eliminar la variable imp_financ y evitar posibles problemas de colinealidad con num_dt la cual decidi categorizar en si tiene o no líneas en impago interpretando NaN como clientes sin deudas con la compañía.
Re-check for missing data.
Debido al número de niveles que aparecen en la variable provincia (50 Niveles) decidó agrupar cada provincia con respecto a su correspondiente Comunidad Autonóma como una forma más eficiente de re categorizár dichas variables.
A partir de la información suministrada por el Instituto Nacional de Estadística https://www.ine.es/daco/daco42/codmun/cod_ccaa_provincia.htm, obtengo la información de la web como un diccionario y el proceso conciste en hallar coincidencia con cada columna (recuperada y actual) por el hecho de que algunas provincias recuperadas están escritas de forma diferente a las que tengo en mi marco de datos, una vez solucionado esto determino como clave de mi diccionario la lista de provincias actualizada a su escritura coincidente y como valor las diferentes comunidades autónomas. Con la ayuda de la función zip que funciona como iterador almaceno la información en mi diccionario después procedi a mapear la información recuperada y agrupada y así crear una nueva variable transformada llamada CCAA.
Automatizar este proceso evita el crear un diccionario manualmente y errores al introducir la información.
def make_soup(url: str) -> BeautifulSoup:
res = requests.get(url)
res.raise_for_status()
return BeautifulSoup(res.text, 'html.parser')
def extract_purchases(soup: BeautifulSoup) -> list:
table = soup.find('th', text=re.compile('Provincia')).find_parent('table')
purchases = []
for row in table.find_all('tr')[1:]:
Cca_cell,pro_cell= row.find_all('td')[::-2]
p = {
'CCAA': pro_cell.text.strip(),
'Provincia': Cca_cell.text.strip(),
#'CPRO' : cpro_cell.text.strip(),
}
purchases.append(p)
return purchases
if __name__ == '__main__':
url = 'https://www.ine.es/daco/daco42/codmun/cod_ccaa_provincia.htm'
soup = make_soup(url)
purchases = extract_purchases(soup)
from pprint import pprint
pprint(purchases)
info = pd.DataFrame(purchases)
# renombrando columnas no coincidentes por escritura
info.Provincia.replace({'Balears, Illes':'Islas Baleares','Palmas, Las':'Las Palmas','Girona':'Gerona',
'Lleida':'Lérida','Alicante/Alacant':'Alicante', 'Castellón/Castelló':'Castellón',
'Valencia/València':'Valencia', 'Coruña, A':'La Coruña','Ourense':'Orense',
'Araba/Álava':'Álava', 'Bizkaia':'Vizcaya','Gipuzkoa':'Guipúzcoa',
'Rioja, La':'La Rioja'},inplace=True)
# Acortando Títulos largos para mejor ajuste en visualización
info.CCAA.replace({'Asturias, Principado de':'Asturias','Balears, Illes':'Balears',
'Madrid, Comunidad de':'Madrid','Murcia, Región de':'Murcia',
'Navarra, Comunidad Foral de':'Navarra','Rioja, La': 'Rioja'},inplace=True)
info.drop([50,51,52],axis=0,inplace=True)
# Actualizando Diccionario
clave=info['Provincia']
valor = info['CCAA']
Dict = dict(zip(clave,valor))
combined_set = [df_dic,df_ene] # combined 2 datasets for more efficient processing
#impFinanc_bins = [13,21,22,25,99999]
#impFinanc_labels = ['13','21','22','25+']
# Extrae información con Split
def get_info(dataset, feature_name):
return dataset[feature_name].map(lambda name:name.split('/')[0].split('/')[0].strip())
# Extrae información mensual
def get_month(dataset, feature_name):
return pd.to_datetime(dataset[feature_name]).map(lambda x: x.month)
# Extrae información año columna actual
def get_year(dataset, feature_name):
return pd.to_datetime(dataset[feature_name]).map(lambda x: x.year)
# Extrae los meses que han transcurrido desde la fecha de alta
# hasta la fecha actual.
def diff_month(dataset, d2):
x=datetime.now()
dataset[d2] = pd.to_datetime(dataset[d2])
return ((x.year - dataset[d2].dt.year) * 12 + (x.month - dataset[d2].dt.month)-1).map(lambda x: x)
# Agrupa Categórias
def cut_levels(x, threshold, new_value):
value_counts = x.value_counts()
labels = value_counts.index[value_counts < threshold]
x.loc[np.in1d(x, labels)] = new_value
return x
for dataset in combined_set:
dataset['num_lineas']= dataset['num_lineas'].astype(object)
dataset['InfoNumdt'] = dataset['num_dt'].notnull().astype(int)
dataset['Month_antig'] = diff_month(dataset, 'antiguedad')
dataset['CCAA'] = dataset['provincia'].map(Dict)
cat_plot(df_dic, 'num_lineas', '_merge', survival_palette)
cat_plot(df_dic, 'InfoNumdt', '_merge', survival_palette)
plot = pd.crosstab(index=df_dic['_merge'],
columns=df_dic['InfoNumdt']
).apply(lambda r: r/r.sum() *100,
axis=0).plot(kind='bar', stacked=True)
Parece que convertir el atributo a binaria fue la mejor opción para encontrar diferencias que agrupen a cada cliente. Por tanto los clientes de 1 a 4 líneas en impago parecen no permanecer en la compañía. Esto es interesante ya que contrarío a lo que se cree, aunque que el cliente tenga pagos pendientes no se le podrá negar el cambio de compañía. El cliente con una terminal móvil si tiene un pago a plazos, tendrá que liquidar las cuotas pendientes cuando haga la portabilidad, en la última factura.
for dataset in combined_set:
dataset['diff_Month'] = ''
dataset.loc[dataset['Month_antig'] < 24, 'diff_Month'] = '-2'
dataset.loc[(dataset['Month_antig'] >= 24) & (dataset['Month_antig'] <= 84), 'diff_Month'] = '2-7'
dataset.loc[(dataset['Month_antig'] > 84 ) & (dataset['Month_antig'] <= 120), 'diff_Month'] = '7-10'
dataset.loc[(dataset['Month_antig'] > 120 ) & (dataset['Month_antig'] <= 240), 'diff_Month'] = '10-20'
dataset.loc[dataset['Month_antig'] > 240, 'diff_Month'] = '20+'
cat_plot(df_dic, 'diff_Month', '_merge', survival_palette)
fig, axis = plt.subplots(1,1,figsize=(28,5))
sns.countplot(x='CCAA', hue='_merge', data=df_dic,
palette=survival_palette,ax=axis)
axis.set_ylim(0,10000)
plt.show()
print(df_dic['CCAA'].value_counts())
Podemos ver que hay una diferencia en la supervivencia en cada nueva característica.
Limpié las características no utilizadas o transformadas y dividí la columna _merge en una serie, por lo que tanto el conjunto de datos de entrenamiento como el conjunto de datos de prueba son idénticos.
df_dic.set_index('id', inplace=True)
df_ene.set_index('id',inplace=True)
df_train= df_dic.drop(['_merge','provincia','antiguedad','num_dt',
'Month_antig'],axis=1)
df_test = df_ene.drop(['provincia','antiguedad','num_dt','Month_antig'],axis=1)
y_train = df_dic['_merge'] # Relocate Survived target feature to y_train
X_train_analysis = df_train.copy()
#Codificación Etiquetas con LabelEncoder
lb_make = LabelEncoder()
#Clientes
X_train_analysis['incidencia'] = X_train_analysis['incidencia'].map({'NO': 0, 'SI': 1}).astype(int)
X_train_analysis['CCAA'] = lb_make.fit_transform(X_train_analysis['CCAA'])
X_train_analysis['diff_Month'] = lb_make.fit_transform(X_train_analysis['diff_Month'])
X_train_analysis['num_lineas'] = lb_make.fit_transform(X_train_analysis['num_lineas'])
#Productos
X_train_analysis['TV'] = X_train_analysis['TV'].map({'tv-futbol': 0, 'tv-familiar': 1, 'tv-total': 2}).astype(int)
X_train_analysis['conexion'] = X_train_analysis['conexion'].map({'ADSL': 0, 'FIBRA': 1}).astype(int)
X_train_analysis['vel_conexion'] = lb_make.fit_transform(X_train_analysis['vel_conexion'])
#Financiación
X_train_analysis['financiacion'] = X_train_analysis['financiacion'].map({'SI': 0, 'NO': 1}).astype(int)
X_train_analysis['descuentos'] = X_train_analysis['descuentos'].map({'SI': 0, 'NO': 1}).astype(int)
X_test_analysis = df_test.copy()
#Codificación Etiquetas con LabelEncoder
lb_make = LabelEncoder()
#Clientes
X_test_analysis['incidencia'] = X_test_analysis['incidencia'].map({'NO': 0, 'SI': 1}).astype(int)
X_test_analysis['CCAA'] = lb_make.fit_transform(X_test_analysis['CCAA'])
X_test_analysis['diff_Month'] = lb_make.fit_transform(X_test_analysis['diff_Month'])
X_test_analysis['num_lineas'] = lb_make.fit_transform(X_test_analysis['num_lineas'])
#Productos
X_test_analysis['TV'] = X_test_analysis['TV'].map({'tv-futbol': 0, 'tv-familiar': 1, 'tv-total': 2}).astype(int)
X_test_analysis['conexion'] = X_test_analysis['conexion'].map({'ADSL': 0, 'FIBRA': 1}).astype(int)
X_test_analysis['vel_conexion'] = lb_make.fit_transform(X_test_analysis['vel_conexion'])
#Financiación
X_test_analysis['financiacion'] = X_test_analysis['financiacion'].map({'SI': 0, 'NO': 1}).astype(int)
X_test_analysis['descuentos'] = X_test_analysis['descuentos'].map({'SI': 0, 'NO': 1}).astype(int)
Cuando 2 características o más tienen correlación, eso significa que se están explicando entre sí al tiempo que brindan solo una pequeña o ninguna información nueva. Las características con correlación conducirían a un sobreajuste en el modelo de aprendizaje automático, lo que podría dar como resultado una alta precisión en el conjunto de datos de entrenamiento y disminuir la precisión en el conjunto de datos de prueba.
Pero incluso si existen correlaciones, no podemos reducir descuidadamente las características. Como vi comentarios de Anton Lytyakov y GeekYoung. LongYin La lección que aprendí es no eliminar features juzgando demasiado pronto, al menos hasta que termine el análisis.
colormap = plt.cm.viridis
plt.figure(figsize=(14,14))
plt.title('Correlation between Features', y=1.05, size = 30)
sns.heatmap(X_train_analysis.corr(),
linewidths=0.2,
vmax=2.0,
square=True,
cmap=colormap,
linecolor='white',
annot=True)
features_num_train = X_train_analysis
def calculateVIF(features_num):
features = list(features_num.columns)
num_features = len(features)
model = LinearRegression()
result = pd.DataFrame(index = ['VIF'], columns = features)
result = result.fillna(0)
for ite in range(num_features):
x_features = features[:]
y_featue = features[ite]
x_features.remove(y_featue)
x = features_num[x_features]
y = features_num[y_featue]
model.fit(features_num[x_features],features_num[y_featue])
result[y_featue] = 1/(1 - model.score(features_num[x_features],features_num[y_featue]))
return result
num_vif = features_num_train.copy(deep = True)
features = list(num_vif.columns)
num_vif = num_vif[features]
calculateVIF(num_vif)
No se observa Problemas de Multicolinealidad
X_test_analysis.head(7)
X_train_analysis.head(7)
Otra forma de analizar cuánto impacto podría tener una característica en la variable objetivo(No permanencia) es ver la importancia de cada características usando diferentes algoritmos en este caso lo haré con RandomForest y Árbol de decisión de manera aleatoría.
def automatic_selection(clas,dat_train,name):
importances=clas.feature_importances_
std = np.std([clas.feature_importances_ for tree in clas.estimators_],
axis=0)
indices = np.argsort(importances)[::-1]
sorted_important_features=[]
predictors=dat_train.columns
for i in indices:
sorted_important_features.append(predictors[i])
plt.figure(figsize=(12,5))
plt.title(f"Feature Importance{name}",fontsize=20)
plt.bar(range(np.size(predictors)), importances[indices],
color="red", yerr=std[indices], align="center")
plt.xticks(range(np.size(predictors)), sorted_important_features, rotation='vertical')
plt.xlim([-1, np.size(predictors)])
return plt
rforest_checker = RandomForestClassifier(random_state = 0)
rforest_checker.fit(X_train_analysis, y_train)
importances_df = pd.DataFrame(rforest_checker.feature_importances_, columns=['Feature_Importance'],
index=X_train_analysis.columns)
importances_df.sort_values(by=['Feature_Importance'], ascending=False, inplace=True)
print(importances_df)
automatic_selection(rforest_checker,X_train_analysis,"RandomForest")
rtree_checker=ExtraTreesClassifier(random_state = 0,n_jobs=1)
rtree_checker.fit(X_train_analysis, y_train)
importances_df = pd.DataFrame(rtree_checker.feature_importances_, columns=['Feature_Importance'],
index=X_train_analysis.columns)
importances_df.sort_values(by=['Feature_Importance'], ascending=False, inplace=True)
print(importances_df)
automatic_selection(rtree_checker,X_train_analysis,"TreeClassifier")
Aunque el resultado aquí es bastante aleatorio parece que ambos algoritmos coinciden en que las variables InfoNumdt,incidencia, descuentos, como las más importantes.
En esta parte, seleccionaré características basandome en diferentes métodos que consisten en eliminación de características con varianza constante, selección de características univariadas, eliminación recursivas de características (RFE), eliminación recursivas de características con validación cruzada (RFECV) y selección de características basadas en árboles. Construire un modelo de clasificación para predecir utilizando como algoritmo RandomForest.
## métricas
def saca_metricas(y1, y2):
false_positive_rate, recall, thresholds = roc_curve(y1, y2)
roc_auc = auc(false_positive_rate, recall)
print('AUC')
print(roc_auc)
plt.plot(false_positive_rate, recall, 'b')
plt.plot([0, 1], [0, 1], 'r--')
plt.title('AUC = %0.2f' % roc_auc)
#funcion para mostrar los resultados
def mostrar_resultados(y_test, pred_y):
conf_matrix = confusion_matrix(y_test, pred_y)
plt.figure(figsize=(8,8))
sns.heatmap(conf_matrix, xticklabels=True, yticklabels=True, annot=True, fmt="d");
plt.title("Confusion matrix")
plt.ylabel('True class')
plt.xlabel('Predicted class')
plt.show()
print (classification_report(y_test, pred_y))
def draw_confusion_matrices(confusion_matricies,class_names):
class_names = class_names.tolist()
for cm in confusion_matrices:
classifier, cm = cm[0], cm[1]
sns.heatmap(cm, xticklabels=True, yticklabels=True, annot=True,fmt="d");
plt.title('Confusion matrix for %s' % classifier)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()
# ya lo había hecho anteriormente pero soló para no olvidar lo comento aquí
target=df_dic['_merge']
target.unique()
target.head()
X_train, X_test, y_train, y_test = train_test_split(X_train_analysis,
target,
test_size=0.2,
random_state=56,
stratify = target)
print(X_train.shape,X_test.shape)
Con el fin de encontrar aquellas características que no proporcionan información a mi modelo y lo limiten encuanto a discriminar o predecir la variable objetivo. Para identificar características constantes, usaré la función VarianceThreshold de sklearn.
Su función es eliminar todas las features cuya variación no alcanza algún umbral. Por defecto, elimina todas las features de variación cero, es decir, las características que tienen el mismo valor en todas las muestras.
varModel=VarianceThreshold(threshold=0) #Estableciendo umbral de variación a 0
varModel.fit(X_train)
constArr=varModel.get_support()
#get_support() retorna True y False value para cada feature.
#True: Not a constant feature
#False: Constant feature
# Contando el número de features constantes y no constantes
collections.Counter(constArr)
#Non Constant feature:17
# Por tanto No hay features constantes
Este método consiste en tomar como parámetro una función de puntuación , en este caso calculará la estadística chi2 entre cada característica. Un valor pequeño índicara que la variable es independiente de "y" , por otro lado un valor grande significará que la variable no está relacionada aleatoriamente con "y". En este caso el parámetro k conserva las características cuyos valores sean distintos de "y", el número de características almacenadas deberá proporcionarse en este caso eligiré 5.
Documentación : http://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SelectKBest.html#sklearn.feature_selection.SelectKBes
# find best scored 5 features
select_feature = SelectKBest(chi2, k=5).fit(X_train, y_train)
print('Score list:', select_feature.scores_)
print('Feature list:', X_train.columns)
atrib = select_feature.get_support()
atributos = [X_train.columns[i] for i in list(atrib.nonzero()[0])]
atributos
Este algoritmo selecciona a los mejores atributos basándose en una prueba estadística univariante. Al objeto SelectKBest le pasamos la prueba estadística chi2, en este caso una junto con el número de atributos a seleccionar k=5. El algoritmo va a aplicar la prueba a todos los atributos y va a seleccionar los que mejor resultado obtuvieron. Como podemos ver, el algoritmo seleccionó la cantidad de atributos que le indique; en este ejemplo decidí seleccionar solo 5.
Este método me selecciona como las 5 mejores características para clasificar: InfoNumdt, Incidencia,seg_llamad_ent,seg_llamad_sal,descuentos
Aquí entrenare un primer modelo con las 5 mejores caraterísticas seleccionadas por el método Univariado.
X_train_2 = select_feature.transform(X_train)
X_test_2 = select_feature.transform(X_test)
#LogisticRegression classifier with n_estimators=10 (default)
classifier = RandomForestClassifier()
classifier = classifier.fit(X_train_2,y_train)
y_pred_1 = classifier.predict(X_test_2)
mostrar_resultados(y_test, y_pred_1)
saca_metricas(y_test, y_pred_1)
score_1 = classifier.score(X_train_2,y_train)
#ac_2 = accuracy_score(y_test,classifier.predict(X_test_2))
#print('Accuracy is: ',ac_2)
#cm_2 = confusion_matrix(y_test,classifier.predict(X_test_2))
#sns.heatmap(cm_2,annot=True,fmt="d")
http://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.RFE.html Básicamente, utiliza uno de los métodos de clasificación (RandomForest en nuestro ejemplo), y asigna pesos a cada una de las características. Los pesos absolutos más pequeños se eliminan de las características del conjunto actual. Ese procedimiento se repite de forma recursiva en el conjunto podado hasta el número deseado de características.
# Create the RFE object and rank each pixel
classifier_2 = RandomForestClassifier()
rfe = RFE(estimator=classifier_2, n_features_to_select=5, step=1)
rfe = rfe.fit(X_train, y_train)
print('Chosen best 5 feature by rfe:',X_train.columns[rfe.support_])
Las 5 mejores características elegidas por rfe son incidencia,mb_datos,seg_llamad_sal, descuentos,nfoNumdt. Como no son exactamente similares con el método anterior (selectkBest) al diferir en 1 característica. Por lo tanto, calcularé la precisión nuevamente. En breve, podre determinar si realicé una buena selección de funciones con los métodos rfe y selectkBest. Sin embargo, encontré las 5 mejores característica con dos métodos diferentes. pero no necesariamente deben ser 5 características, quizás alomejor con tan sólo 2 características o 15, se obtendría la misma precisión o mejor precisión, por lo tanto veamos cuántas características necesito usar con el método rfecv.
X_train_3 = rfe.transform(X_train)
X_test_3 = rfe.transform(X_test)
#LogisticRegression classifier with n_estimators=10 (default)
classifier_2= RandomForestClassifier()
classifier_2= classifier_2.fit(X_train_3,y_train)
y_pred = classifier_2.predict(X_test_3)
mostrar_resultados(y_test, y_pred)
saca_metricas(y_test, y_pred)
score_2 = classifier_2.score(X_train_3,y_train)
Por lo visto al diferir sólo en una variable la precisión aduraspenas llega a afectarse
score = classifier_2.score(X_train_3,y_train)
print("Metrica del modelo", score)
accuracy_score(y_test, y_pred)
score = classifier_2.score(X_train_3,y_train)
#print("Metrica del modelo", score)
#recall_score(y_test, y_pred)
Con este método no solo encontraré las mejores características, sino también cuántas necesitaré para una mayor precisión. http://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.RFECV.html
# La puntuación de "precisión" es proporcional al número de clasificaciones correctas
classifier_3 = RandomForestClassifier()
rfecv = RFECV(estimator=classifier_3, step=1, cv=10,scoring='accuracy') #5-fold cross-validation
rfecv = rfecv.fit(X_train, y_train)
print('Optimal number of features :', rfecv.n_features_)
print('Best features :', X_train.columns[rfecv.support_])
# Plot number of features VS. cross-validation scores
import matplotlib.pyplot as plt
plt.figure()
plt.xlabel("Number of features selected")
plt.ylabel("Cross validation score of number of selected features")
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_)
plt.show()
A medida que aumenta el número de variables aumenta la precisión del modelo.
clf_rf_5 = RandomForestClassifier()
clr_rf_5 = clf_rf_5.fit(X_train,y_train)
importances = clr_rf_5.feature_importances_
std = np.std([tree.feature_importances_ for tree in clf_rf_5.estimators_],
axis=0)
indices = np.argsort(importances)[::-1]
# Print the feature ranking
print("Feature ranking:")
for f in range(X_train.shape[1]):
print("%d. feature %d (%f)" % (f + 1, indices[f], importances[indices[f]]))
# Plot the feature importances of the forest
plt.figure(1, figsize=(14, 13))
plt.title("Feature importances")
plt.bar(range(X_train.shape[1]), importances[indices],
color="g", yerr=std[indices], align="center")
plt.xticks(range(X_train.shape[1]), X_train.columns[indices],rotation=90)
plt.xlim([-1, X_train.shape[1]])
plt.show()
>
x_labels = ('Algorithm','Precision','Recall','Overal')
y_labels = ('Modelo_1','Modelo_2')
score_array = np.array([['RandomForest',precision_score(y_test, y_pred_1),
recall_score(y_test, y_pred_1), f1_score(y_test, y_pred_1)],
['RandomForest',precision_score(y_test, y_pred),
recall_score(y_test, y_pred), f1_score(y_test, y_pred)]])
fig = plt.figure(1)
fig.subplots_adjust(left=0.002,top=0.8, wspace=1)
ax = plt.subplot2grid((4,3), (1,1), colspan=2, rowspan=2)
score_table = ax.table(cellText=score_array,
rowLabels=y_labels,
colLabels=x_labels,
loc=
'upper center')
score_table.set_fontsize(22)
ax.axis("off") # Hide plot axis
fig.set_size_inches(w=25, h=13)
plt.title('Comparación de Modelos Iniciales', fontdict = {'fontsize' : 20})
plt.show()
x_labels = ('Accuracy','Score')
y_labels = ('Modelo_1','Modelo_2')
score_array = np.array([[accuracy_score(y_test, y_pred_1),score_1],
[accuracy_score(y_test, y_pred),score_2]])
fig = plt.figure(1)
fig.subplots_adjust(left=0.002,top=0.8, wspace=1)
ax = plt.subplot2grid((4,3), (1,1), colspan=2, rowspan=2)
score_table = ax.table(cellText=score_array,
rowLabels=y_labels,
colLabels=x_labels,
loc=
'upper center')
score_table.set_fontsize(22)
ax.axis("off") # Hide plot axis
fig.set_size_inches(w=25, h=13)
plt.title('Comparación de Modelos Iniciales', fontdict = {'fontsize' : 20})
plt.show()
1) Tipo de Algoritmo Utilizado: RandomForest Al análisis de distribución de clases inicial donde se presenta un desequilibrio de clases con un sesgo hacía la etiqueta de clase permanencia cuya clase es mayoritaria en aproximandamente un 93% con una gran diferencia sobre la clase minoritaria cuya representación es del 7 % ,en general esto afecta a los algoritmos en su proceso de generalización de la información y perjudica a las clases minoritarias al no lograr diferenciar de una clase a otra y se limite a beneficiar siempre a la clase mayoritaria. Por ello decidí trabajar inicialmente con el algoritmo RandomForest el cual No es superable en precisión, de entre los algoritmos actuales, y mediante una combinación de árboles de decisión mejora la precisión en la clasificación mediante la incorporación de aleatoriedad en la construcción de cada clasificador individual y a su vez aporta estimaciones de qué variables son importantes en la clasificación.
2) Varibles Seleccionadas: Con el fin de mejorar la capacidad predictiva de mi modelo así como reducir su complejidad acudí a las tecnicas detalladas anteriormente evitando el no seleccionar predictores que podrían ser importantes e introducir sesgo en mi modelo,o todo lo contrarío obtener un modelo excesivamente especificado con variables predictoras redundantes y con estimaciones poco precisas.
En el proceso de selección de atributos las 5 variables más relevantes fuerón InfoNumdt,incidencia,descuentos,seg_llamad_sal y mb_datos. Para tener un modelo más simple y mucho mas ameno de explicar decidido incluir en mi modelo 8 variables que en su mayoría 3 de ellas están relacionadas con el consumo (InfoNumdt,incidencia,descuentos,seg_llamad_sal,seg_llamad_ent,mb_datos,financiación y facturación) lo que se reduce a una mejor comprensión de los problemas que quiero abordar y un mejor conocimiento de dominio del negocio en cuestión en cuanto a las estrategías comerciales que deberán abordar en telefóníca.
3) Métricas Obtenidas Explicación Modelo:
Precisión: Mi modelo tiene una precisión del 78% en la No permanencía de los clientes es decir,cuando mi modelo hace una predicción, la frecuencia con la que es correcto . Al responder a la pregunta ¿Que cantidad de clientes se irán de la compañía?, acertará un 78% y se equivocará un 22% de las veces cuando prediga que un cliente se irá de la compañía.Por lo tanto la calidad del modelo aparentemente es buena.
Recall(Exhaustividad): Está métrica me informa sobre la cantidad real que el modelo es capaz de identificar, por tanto la exhaustividad es la respuesta a ¿que porcentaje de los clientes que se irán de la compañía somos capaces de identificar?
Es decir, el modelo es capaz de identificar correctamente un 91% de los clientes que decidirán No permanecer en la compañía. Esto significa que el modelo podría identifica 6 de cada 7 clientes que se marcharán de telefónica.
F1:El valor F1 se utiliza para combinar las medidas de precision y recall (precisión y sensibilidad en una sola métrica.)en un sólo valor. lo que hace más fácil el poder comparar el rendimiento combinado de la precisión y la exhaustividad entre varias soluciones. Mi modelo al tener alta precisión y bajo recall es decir mi modelo no detecta la clase muy bien pero cuando lo hace es altamente confiable. Al tener un dataset con desequilibrio, suele ocurrir que se obtiene un alto valor de precisión en la clase Mayoritaria y un bajo recall en la clase Minoritaria.
Accuracy(Exactitud):Está métrica mide el porcentaje de casos en que mi modelo ha acertado es decir un 97% aunque está métrica es de mucho cuidado puede hacer que un modelo malo parezca que es mucho mejor de lo que es. por ello este modelo necesitará algunas otras validaciones. Como el accuracy en Test es cercano al conjunto de entrenamiento quiere decir que el modelo entrenado está bien generalizado de no ser así podría ser un indicador de Overfitting.
Planteamiento Problema: Ante una precisión del 78% y Exhaustividad del 91% los falsos positivos aumentan y los falsos negativos disminuyen. Como resultado, esta vez la precisión disminuye y aumenta la exhaustividad. Al tratar con desbalance de clases es comveniente mayor Exhaustividad ya que nos interesa incrementar las clasificaciones correctas en la clase 2 (que son los clientes en riesgo de fuga de la compañía) por tanto se debe minimizar a toda costa un incremento en el error de Tipo II, la compañía perderá mucho dinero sino hay una detección temprana de los clientes que se fugaran y no podrían implementar estrategías comerciales a tiempo y esto indudablemente reduce sus posibilidades de recuperar al cliente en cuestión. En otras palabras detectar a tiempo cualquier síntoma que indique una posible fuga de clientes es determinante para la capacidad de reacción de las empresas porque una vez se han ido a la competencia, recuperarlos es extremadamente complejo y costoso económicamente Hay que tener en cuenta que es más difícil y caro captar clientes nuevos o tratar de recuperar clientes ya perdidos que retener a los clientes actuales.
Conclusión:
-Con el planteamiento del problema en cuestión y las métricas obtenidas decido como métrica de Negocio el recall, como lo explique anteriormente.
-Al tratarse de un conjunto de datos en desequilibrio la precisión de clasificación puede ser engañosa por tanto tendré que verificar si mejora con un ajuste de hiperparámetros con algún tipo de penalización. Hay que tener encuenta que el entrenamiento y validación es sobre el mismo conjunto de datos, por tanto los algoritmos ya se han entrenado sobre un conjunto de datos generalizado en test, la prueba de fuego será cuando intenten predecir datos nuevos (Cosecha Enero) a partir de crear un nuevo modelo entrenando con todos los datos del dataset diciembre.
La precisión no es la métrica a utilizar cuando se trabaja con un conjunto de datos desequilibrado , por tanto debo fijarme en medidas de integridad de un clasificador o en el promedio ponderado de precisión y recuperación.
y como se puede ver en la matriz de confusión hace pocas predicciones erróneas. Los Falsos negativos son 207(es decir 207 clientes que se predicen erróneamente como clientes que permanecerán en la compañía cuando en realidad no es así) en comparación con los falsos positivos que son 339(es decir 339 clientes que no se irán de la compañía fuerón predecidos erróneamente como clientes que Si se marcharán), en cuanto a las 17338 observaciones son los clientes que permanecerán en la compañía clasificados correctamente y 1210 observaciones clasificadas correctamente como clientes que no permanecerán en la compañía.
X_train = X_train.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
X_test = X_test.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
X_test_analysis = X_test_analysis.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
X_train_analysis = X_train_analysis.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
df_train = df_train.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
df_test = df_test.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
#####
X_train[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']] = X_train[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']].apply(zscore)
X_test[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']] = X_test[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']].apply(zscore)
## conjunto de validación
X_test_analysis[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']] = X_test_analysis[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']].apply(zscore)
X_train_analysis[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']] = X_train_analysis[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']].apply(zscore)
####
df_train[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']] = df_train[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']].apply(zscore)
df_test[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']] = df_test[['mb_datos','seg_llamad_ent',
'seg_llamad_sal','facturacion']].apply(zscore)
X_test_analysis.head(7)
X_train_analysis.head(7)
Crearé un clasificador usando el algoritmo k-Nearest Neighbours. Primero observaré las precisiones para diferentes valores de k.
#Setup arrays to store training and test accuracies
neighbors = np.arange(1,9)
train_accuracy =np.empty(len(neighbors))
test_accuracy = np.empty(len(neighbors))
for i,k in enumerate(neighbors):
#Setup a knn classifier with k neighbors
knn = KNeighborsClassifier(n_neighbors=k)
#Fit the model
knn.fit(X_train, y_train)
#Compute accuracy on the training set
train_accuracy[i] = knn.score(X_train, y_train)
#Compute accuracy on the test set
test_accuracy[i] = knn.score(X_test, y_test)
plt.title('k-NN Varying number of neighbors')
plt.plot(neighbors, test_accuracy, label='Testing Accuracy')
plt.plot(neighbors, train_accuracy, label='Training accuracy')
plt.legend()
plt.xlabel('Number of neighbors')
plt.ylabel('Accuracy')
plt.show()
Se tiene la máxima precisión en test a partir de k = 7. Entonces, crearé un KNeighboursclassifier con un número de vecinos igual a 7.
knn = KNeighborsClassifier(n_neighbors=7)
knn=knn.fit(X_train, y_train)
y_pred_knn1 = knn.predict(X_test)
mostrar_resultados(y_test, y_pred_knn1)
y_pred_knn1 = knn.predict(X_test_analysis)
y_prob_knn1 = knn.predict_proba(X_test_analysis)
from funcion_proba import clientes_proba
clientes_proba(y_pred_knn1, y_prob_knn1 , "KNeighborsClassifier_1")
def run_model_balanced(X_Train,y_Train):
clf = LogisticRegression(C=0.5,penalty='l2',random_state=0,solver="sag",
class_weight="balanced")
clf.fit(X_Train,y_Train)
return clf
model_balc = run_model_balanced(X_train, y_train)
y_pred_balc = model_balc.predict(X_test_analysis)
y_proba_balc = model_balc.predict_proba(X_test_analysis)
clientes_proba(y_pred_balc, y_proba_balc, "LogisticRegressionPenalización")
forest = RandomForestClassifier(n_estimators = 500, criterion = 'entropy', random_state = 0)
forest.fit(X_train, y_train)
y_pred_test_for = forest.predict(X_test_analysis)
y_prob_test_for = forest.predict_proba(X_test_analysis)
clientes_proba(y_pred_test_for, y_prob_test_for , "RForest")
En total 1547 clientes según el modelo estimado con RandomForest se irán de la compañía 17 clientes tienes una probabilidad del 50% de marcharse de la compañía de esos 17 clientes 15 fuerón clasificados en la clase 2 (no permanencia) y 2 de ellos en la clase 1, esta variación en class_predict ocurre por el redondeo en las probabilidades para poder agrupar los clientes.
Los algoritmos de aprendizaje automático no pueden procesar texto y variables categóricas, a menos que tengan una función incorporada. Entonces tenemos que convertir las variables categóricas en variables numéricas. Pero codificarlos en valores ordinales podría hacer que el algoritmo se sesgue sobre qué tan grandes son los números, es por eso que lo haré de ambas formas.
x_train = pd.get_dummies(df_train, columns=['incidencia','financiacion','descuentos','InfoNumdt'])
x_test = pd.get_dummies(df_test, columns=['incidencia','financiacion','descuentos','InfoNumdt'])
x_train.columns
La multicolinealidad es lo que queremos evitar al usar variables ficticias. Cuando 2 o más variables están fuertemente correlacionadas entre sí, por ejemplo, la función 'descuentos' cuando se convierte se convierte en variables ficticias descuento_SI y descuento_NO. Cuando descuento_NO= 0, descuento_SI siempre es 1 y viceversa. En otras palabras, tenemos variables duplicadas que siempre son diferentes. Esta situación también se llama "Trampa variable variable simulada".
Podemos evitar esto excluyendo cualquier 1 ficticio para cada característica. En este caso, elegí eliminar la primera variable ficticia de cada característica.
x_train = x_train.drop(['incidencia_NO','financiacion_SI','descuentos_SI',
'InfoNumdt_0',], axis=1)
x_test = x_test.drop(['incidencia_NO','financiacion_SI','descuentos_SI',
'InfoNumdt_0',], axis=1)
x_train.shape
knn = KNeighborsClassifier(n_neighbors=9)
knn=knn.fit(x_train, target)
y_pred_kndum = knn.predict(x_test)
y_prob_kndum = knn.predict_proba(x_test)
clientes_proba(y_pred_kndum , y_prob_kndum , "KNeighborsClassifier_2")
Hay 3 opciones para probar la precisión de cada clasificador para ver cuál tiene la mejor precisión y la menor varianza.
Usar el conjunto de entrenamiento para entrenar e intentar predecir el mismo conjunto de entrenamiento.
Dividir el nuevo conjunto de entrenamiento y el conjunto de prueba del conjunto de entrenamiento actual (en este caso, X_train e y_train).
Utilizar la técnica de validación cruzada k-fold.
La primera opción tiende a ofrecer una alta precisión, pero no porque el modelo sea bueno, sino porque el modelo está tratando de predecir los datos, lo que ya ha visto. Esta opción debe evitarse a toda costa.
La segunda opción es bastante simple de hacer usando la biblioteca train_test_split, pero el problema es que a veces el conjunto de entrenamiento y el conjunto de datos no se dividen de manera uniforme y nos dan datos muy sesgados. Por ejemplo, un nuevo conjunto de entrenamiento con Num_líneas= 1 y 2 pero no tiene Num_Líneas = 3 en absoluto. Lo que podría hacernos juzgar mal sobre qué modelo es el mejor.
La tercera opción, k-fold Cross Validation, divide los datos de entrenamiento en k divisiones, luego entrena y realiza predicciones k veces. Esta técnica nos ayuda a reducir la posibilidad de sobreajuste y datos sesgados.
A continuación se detallan los procedimientos para utilizar la validación cruzada de k-fold. A partir de aquí crearé cada modelo en base a todo el conjunto de diciembre.
A continuación, voy a entrenar y validar estos modelos de clasificación.
logreg = LogisticRegression()
logreg.fit(X_train_analysis,target)
acc_logreg = cross_val_score(estimator = logreg, X = X_train_analysis, y = target, cv = 10)
logreg_acc_mean = acc_logreg.mean()
logreg_std = acc_logreg.std()
n_neighbours es el número de vecinos, que utiliza el algoritmo para decidir qué clasificación se debe asignar a cada dato. señale n_neighbours = 5, que es el valor predeterminado.
metric = 'minkowski' y p = 2 ahora estoy usando la distancia euclidiana para juzgar la distancia entre los datos.
knn = KNeighborsClassifier(n_neighbors = 5, metric = 'minkowski', p = 2 )
knn.fit(X_train_analysis,target)
acc_knn = cross_val_score(estimator = knn, X = X_train_analysis, y = target, cv = 10)
knn_acc_mean = acc_knn.mean()
knn_std = acc_knn.std()
ksvm = SVC(kernel = 'rbf', random_state = 0)
ksvm.fit(X_train_analysis,target)
acc_ksvm = cross_val_score(estimator = ksvm, X = X_train_analysis, y = target, cv = 10)
ksvm_acc_mean = acc_ksvm.mean()
ksvm_std = acc_ksvm.std()
naive = GaussianNB()
naive.fit(X_train_analysis,target)
acc_naive = cross_val_score(estimator = naive, X = X_train_analysis, y = target, cv = 10)
naive_acc_mean = acc_naive.mean()
naive_std = acc_naive.std()
dtree = DecisionTreeClassifier(criterion = 'gini', random_state = 0)
dtree.fit(X_train_analysis,target)
acc_dtree = cross_val_score(estimator = dtree, X = X_train_analysis, y = target, cv = 10)
dtree_acc_mean = acc_dtree.mean()
dtree_std = acc_dtree.std()
rforest = RandomForestClassifier(n_estimators = 10, criterion = 'gini', random_state = 0)
rforest.fit(X_train_analysis,target)
acc_rforest = cross_val_score(estimator = rforest, X = X_train_analysis, y = target, cv = 10)
rforest_acc_mean = acc_rforest.mean()
rforest_std = acc_rforest.std()
xgb = XGBClassifier()
xgb.fit(X_train_analysis,target)
acc_xgb = cross_val_score(estimator = xgb, X = X_train_analysis, y = target, cv = 10)
xgb_acc_mean = acc_xgb.mean()
xgb_std = acc_xgb.std()
x_labels = ('Accuracy','Deviation')
y_labels = ('Logistic Regression','K-Nearest Neighbors','Kernel SVM','Naive Bayes'
,'Decision Tree','Random Forest','XGBoost')
score_array = np.array([[logreg_acc_mean, logreg_std],
[knn_acc_mean, knn_std],
[ksvm_acc_mean, ksvm_std],
[naive_acc_mean, naive_std],
[dtree_acc_mean, dtree_std],
[rforest_acc_mean, rforest_std],
[xgb_acc_mean, xgb_std]])
fig = plt.figure(1)
fig.subplots_adjust(left=0.2,top=0.8, wspace=1)
ax = plt.subplot2grid((4,3), (0,0), colspan=2, rowspan=2)
score_table = ax.table(cellText=score_array,
rowLabels=y_labels,
colLabels=x_labels,
loc='upper center')
score_table.set_fontsize(14)
ax.axis("off") # Hide plot axis
fig.set_size_inches(w=18, h=10)
plt.show()
Debido a cómo funciona la validación cruzada k-fold, el puntaje de precisión y la desviación estándar para cada ejecución pueden cambiar ligeramente. Por lo tanto, el valor de puntuación que escribí aquí puede no ser exacto, pero tampoco cambiará tanto.
En nuestra ejecución, encontramos que XGBoost tiene la mejor precisión al 98% y la desviación estándar al 0.09% seguido de regresión logística.
Para Naive Bayes,entre todos los algoritmos tiene la precisión mas baja.
Hasta ahora,en los procesos de aprendizaje automático solo utilicé los hiperparámetros predeterminados o seleccionados personalmente (parámetros que podrían cambiarse manualmente, por ejemplo, en Random Forest, n_estimators y criterios). Podría intentar cambiar estos hiperparámetros y volver a ejecutarlos para verificar la precisión. Pero este proceso, cuando se repite varias veces, es realmente lento, por lo que use Grid Search para ayudar a probar diferentes hiperparámetros a la vez.
Los hiperparámetros que podrían ajustarse para mejorar la precisión de la regresión logística son ...
params_logreg = [{'C': [0.01, 0.1, 1, 10, 100], 'penalty': ['l1','l2']}]
grid_logreg = GridSearchCV(estimator = LogisticRegression(),
param_grid = params_logreg,
scoring = 'accuracy',
cv = 10)
grid_logreg = grid_logreg.fit(X_train_analysis,target)
best_acc_logreg = grid_logreg.best_score_
best_params_logreg = grid_logreg.best_params_
Estudié sobre k-NN de muchas fuentes y descubrí que la distancia de Hamming es realmente buena para los datos binarios (y datos categóricos cuando se convierten en variables ficticias), por lo que esta es una buena oportunidad para intentar cambiar la métrica de distancia.
params_knn = [{'n_neighbors': [7,9,10,11,12,15,20], 'metric': ['minkowski','hamming']}]
grid_knn = GridSearchCV(estimator = KNeighborsClassifier(),
param_grid = params_knn,
scoring = 'accuracy',
cv = 10)
grid_knn = grid_knn.fit(X_train_analysis,target)
best_acc_knn = grid_knn.best_score_
best_params_knn = grid_knn.best_params_
X_train_norm = X_train_analysis.copy()
X_train_norm[['incidencia',
'financiacion',
'descuentos',
'InfoNumdt']] = X_train_norm[['incidencia',
'financiacion',
'descuentos',
'InfoNumdt']].apply(zscore)
X_train_norm.head()
#params_ksvm = [{'C': [0.1, 1, 10, 100], 'kernel': ['linear']},
# {'C': [0.1, 1, 10, 100], 'kernel': ['rbf'],
# 'gamma': [0.1, 0.2, 0.3, 0.4, 0.5]},
# {'C': [0.1, 1, 10, 100], 'kernel': ['poly'],
# 'degree': [1, 2, 3],
# 'gamma': [0.1, 0.2, 0.3, 0.4, 0.5]}]
#grid_ksvm = GridSearchCV(estimator = SVC(random_state = 0),
# param_grid = params_ksvm,
# scoring = 'accuracy',
# cv = 10,
# n_jobs=-1)
#grid_ksvm = grid_ksvm.fit(X_train_norm, y_train) # Replace X_train with normalized version here
#best_acc_ksvm = grid_ksvm.best_score_
#best_params_ksvm = grid_ksvm.best_params_
Los parámetros para ajustar en Kernel SVM son los siguientes
Cada interior {} es una rama de parámetros que estamos entrenando. Por ejemplo, en la primera rama se está probando un kernel lineal con un valor de C diferente. La segunda rama que estoy probando es kernel rbf con diferentes C y gamma. La tercera rama que estoy tratando con diferentes C, grados y gamma. De esta manera se puede evitar el uso de parámetros innecesarios, en este caso, grado y gamma en el kernel lineal.
Tardo demasiado tiempo su ejecución por tanto no entrenaré ningún modelo con este algoritmo.
El único algoritmo con muy baja precisión en la prueba de validación cruzada anterior. Pero lamentablemente, no pude encontrar ningún hiperparámetro para sintonizar en esta clase.
params_dtree = [{'min_samples_split': [5, 10, 15, 20],
'min_samples_leaf': [1, 2, 3],
'max_features': ['auto', 'log2']}]
grid_dtree = GridSearchCV(estimator = DecisionTreeClassifier(criterion = 'gini',
random_state = 0),
param_grid = params_dtree,
scoring = 'accuracy',
cv = 10,
n_jobs=-1)
grid_dtree = grid_dtree.fit(X_train_analysis,target)
best_acc_dtree = grid_dtree.best_score_
best_params_dtree = grid_dtree.best_params_
igual Decision Tree, pero también con n_estimators.
params_rforest = [{'n_estimators': [100,200, 300,500],
'max_depth': [5, 10, 15, 20],
'min_samples_split': [1,2,3,4]}]
grid_rforest = GridSearchCV(estimator = RandomForestClassifier(criterion = 'gini',
random_state = 0,
n_jobs=-1),
param_grid = params_rforest,
scoring = 'accuracy',
cv = 10,
n_jobs=-1)
grid_rforest = grid_rforest.fit(X_train_analysis,target)
best_acc_rforest = grid_rforest.best_score_
best_params_rforest = grid_rforest.best_params_
grid_score_dict = {'1. Grid Search Score': [best_acc_logreg,best_acc_knn,'-','-',
best_acc_dtree,best_acc_rforest,'(add later)'],
'2. Previous Score': [logreg_acc_mean,knn_acc_mean,'-',naive_acc_mean,
dtree_acc_mean,rforest_acc_mean,xgb_acc_mean],
'3. Optimized Parameters': [best_params_logreg,best_params_knn,'-','-',
best_params_dtree,best_params_rforest,'(add later)'],
}
pd.DataFrame(grid_score_dict, index=['Logistic Regression','K-Nearest Neighbors','Kernel SVM','Naive Bayes',
'Decision Tree','Random Forest','XGBoost'])
#Guardando modelos (Fuerón muy pesados al ejecutarse)
#joblib.dump(grid_logreg, 'modelo_grid_logreg')
#joblib.dump(grid_knn, 'modelo_grid_knn')
#joblib.dump(grid_dtree, 'modelo_grid_dtree')
#joblib.dump(grid_rforest, 'modelo_grid_rforest')
#Cargando Modelos
#grid_logreg = joblib.load('modelo_grid_logreg')
#grid_knn = joblib.load('modelo_grid_knn')
#grid_dtree = joblib.load('modelo_grid_dtree')
#grid_rforest= joblib.load('modelo_grid_rforest')
Debido a que toma demasiado tiempo procesarlo, pongo una lista completa de parámetros para Random Forest aquí, mientras reduzco los parámetros y valores anteriores, pero aún incluyo el mejor.
""" params_rforest = [{'n_estimators': [100, 200, 500, 800],
'min_samples_split': [5, 10, 15, 20],
'min_samples_leaf': [1, 2, 3],
'max_features': ['auto', 'log2']}] """
best_params_logreg
best_params_knn
best_params_dtree
best_params_rforest
Después de entrenar los modelos, mantengo tanto la precisión de predicción en el conjunto de datos de entrenamiento a través de la validación cruzada (y_pred_train) como la predicción en el conjunto de datos de prueba (y_pred_test) para usar en la siguiente sección.
logreg = LogisticRegression(C = 100, penalty = 'l2')
logreg.fit(X_train_analysis, target)
y_pred_train_logreg = cross_val_predict(logreg, X_train_analysis, target)
y_pred_test_logreg = logreg.predict(X_test_analysis)
logreg_1 = LogisticRegression(C = 1, penalty = 'l2')
logreg_1.fit(X_train_analysis, target)
y_pred_train_logreg_1 = cross_val_predict(logreg_1, X_train_analysis, target)
y_pred_test_logreg_1 = logreg_1.predict(X_test_analysis)
knn = KNeighborsClassifier(n_neighbors = 15, metric = "hamming")
knn.fit(X_train_analysis, target)
y_pred_train_knn = cross_val_predict(knn, X_train_analysis, target)
y_pred_test_knn = knn.predict(X_test_analysis)
dtree = DecisionTreeClassifier(criterion = 'gini', max_features='auto', min_samples_leaf=3, min_samples_split=20,
random_state = 0)
dtree.fit(X_train_analysis, target)
y_pred_train_dtree = cross_val_predict(dtree, X_train_analysis, target)
y_pred_test_dtree = dtree.predict(X_test_analysis)
rforest = RandomForestClassifier(max_depth = 10, min_samples_split=3, n_estimators = 100, random_state = 0) # Grid Search best parameters
rforest.fit(X_train_analysis, target)
y_pred_train_rforest = cross_val_predict(rforest, X_train_analysis, target)
y_pred_test_rforest = rforest.predict(X_test_analysis)
#y_test = np.array(y_test)
#class_names = np.unique(y_test)
#confusion_matrices = [
# ("R Logística",confusion_matrix(y_test,y_pred_test_logreg)),
# ("Tree",confusion_matrix(y_test, y_pred_test_dtree)),
# ("Random Forest",confusion_matrix(y_test, y_pred_test_rforest))
#]
# Pyplot code not included to reduce clutter
#draw_confusion_matrices(confusion_matrices,class_names)
y_prob_test_logreg_1 = logreg_1.predict_proba(X_test_analysis)
y_prob_test_knn = knn.predict_proba(X_test_analysis)
y_prob_test_dtree = dtree.predict_proba(X_test_analysis)
y_prob_test_rforest = rforest.predict_proba(X_test_analysis)
y_prob_test_logreg_1 = logreg_1.predict_proba(X_test_analysis)
len(y_pred_test_logreg[0][predict_logreg]*100) y_pred_test_logreg_1
from funcion_proba import clientes_proba
clientes_proba(y_pred_test_logreg_1, y_prob_test_logreg_1, "LogisticRegression_1")
clientes_proba(y_pred_test_knn, y_prob_test_knn, "K-Nearest Neighbors")
clientes_proba(y_pred_test_dtree, y_prob_test_dtree, "DecisionTreeClassifier")
clientes_proba(y_pred_test_rforest, y_prob_test_rforest, "RandomForestClassifier")
data_= y_pred_test_knn
data_= pd.DataFrame(data_)
data_.columns = ["resul"]
data_["resul"].value_counts()
Aprendí esta técnica de Anisotropic. Consulte su notebook para obtener más información. Pero en mi caso, decidí intentar usar predicciones de cross_val_predict en lugar de Out-of-Folds.
second_layer_train = pd.DataFrame( {'Logistic Regression': y_pred_train_logreg_1.ravel(),
'K-Nearest Neighbors': y_pred_train_knn .ravel(),
'Decision Tree': y_pred_train_dtree .ravel(),
'Random Forest': y_pred_train_rforest.ravel()
} )
second_layer_train.head()
X_train_second = np.concatenate(( y_pred_train_logreg_1.reshape(-1, 1), y_pred_train_knn.reshape(-1, 1),
y_pred_train_dtree.reshape(-1, 1), y_pred_train_rforest.reshape(-1, 1)),
axis=1)
X_test_second = np.concatenate(( y_pred_test_logreg_1.reshape(-1, 1), y_pred_test_knn.reshape(-1, 1),
y_pred_test_dtree.reshape(-1, 1), y_pred_test_rforest.reshape(-1, 1)),
axis=1)
xgb = XGBClassifier(
n_estimators= 800,
max_depth= 4,
min_child_weight= 2,
gamma=0.9,
subsample=0.8,
colsample_bytree=0.8,
objective= 'binary:logistic',
nthread= -1,
scale_pos_weight=1).fit(X_train_second,target)
y_pred_ensamble = xgb.predict(X_test_second)
y_prob_ensamble = xgb.predict_proba(X_test_second)
clientes_proba(y_pred_ensamble, y_prob_ensamble, "XGBClassifier")
xgb_1 = XGBClassifier(n_estimators= 800,
max_depth= 4,
min_child_weight= 2,
gamma=0.9,
subsample=0.8,
colsample_bytree=0.8,
random_state = 0)
xgb_1 .fit(X_train_analysis, target)
y_pred_train_xgb_1 = cross_val_predict(xgb_1, X_train_analysis, target)
y_pred_test_xgb_1 = xgb_1.predict(X_test_analysis)
y_prob_test_xgb_1 = xgb_1.predict_proba(X_test_analysis)
clientes_proba(y_pred_test_xgb_1 , y_prob_test_xgb_1, "XGBClassifier_1")
xgb_2 = XGBClassifier(random_state = 0)
xgb_2 .fit(X_train_analysis, target)
y_pred_train_xgb_2 = cross_val_predict(xgb_2, X_train_analysis, target)
y_pred_test_xgb_2 = xgb_2.predict(X_test_analysis)
y_prob_test_xgb_2 = xgb_2.predict_proba(X_test_analysis)
clientes_proba(y_pred_test_xgb_2 , y_prob_test_xgb_2, "XGBClassifier_2")

Ajuste de Parámetros del modelo Ajuste de parametros ó metricas del propio algoritmo para intentar equilibrar a la clase minoritaria penalizando a la clase mayoritaria durante el entrenamiento.
Modificar el Dataset Eliminar muestras de la clase mayoritaria para reducirlo e intentar equilibrar la situación.
Muestras artificiales Intentar crear muestras sintéticas (no idénticas) utilizando diversos algoritmos que intentan seguir la tendencia del grupo minoritario.
Balanced Ensemble Methods Utiliza las ventajas de hacer ensamble de métodos, es decir, entrenar diversos modelos y entre todos obtener el resultado final (por ejemplo «votando») pero se asegura de tomar muestras de entrenamiento equilibradas.
En este ejemplo no realice un muestreo de los datos la cosecha de diciembre ni hice una predicción con los datos de validación de la misma cosecha. Ahora intentaré hacer una misma predicción entrenando un modelo con la cosecha de diciembre y validando con la cosecha de enero.
Utilizaré un parámetro adicional en el modelo de Regresión logística en donde indico weight = «balanced» y con esto el algoritmo se encargará de equilibrar a la clase minoritaria durante el entrenamiento.
def run_model_balanced(X_Train,y_Train):
clf = LogisticRegression(C=1.0,penalty='l2',random_state=0,solver="newton-cg",class_weight="balanced")
clf.fit(X_Train,y_Train)
return clf
model_one = run_model_balanced(X_train_analysis,target)
y_pred_balanc = model_one.predict(X_test_analysis)
y_proba_balanc = model_one.predict_proba(X_test_analysis)
from funcion_proba import clientes_proba
clientes_proba(y_pred_test_xgb_2 , y_prob_test_xgb_2, "Balanced LogisticRegression")
Lo que haré es utilizar un algoritmo para reducir la clase mayoritaria. Lo haré usando un algoritmo que es similar al k-nearest neighbor para ir seleccionando cuales eliminar. Fijemonos que reducimos bestialmente de 88382 muestras de clase cero (la mayoría) y pasan a ser 7085 y Con esas muestras entrenamos el modelo. https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.under_sampling.NearMiss.html
us = NearMiss(sampling_strategy='majority' ,n_neighbors=3, version=2)
X_train_res, y_train_res = us.fit_sample(X_train_analysis,target)
print ("Distribution before resampling {}".format(Counter(target)))
print ("Distribution after resampling {}".format(Counter(y_train_res)))
#model_two= run_model(X_train_res,y_train_res)
model_Near = run_model_balanced(X_train_res, y_train_res)
y_pred_Near = model_Near.predict_proba(X_test_analysis)
y_prob_Near = model_Near.predict_proba(X_test_analysis)
clientes_proba(y_pred_Near , y_prob_Near, "NearMiss")
A pesar de que se redujo considerablemente la clase mayorítaria el modelo no es capaz de predecir nada de la clase 2
En este caso, crearé muestras nuevas «sintéticas» de la clase minoritaria. Usando RandomOverSampler. Y vemos que pasamos de 344 muestras de fraudes a 99.510.
os = RandomOverSampler()
X_train_res, y_train_res = os.fit_sample(X_train_analysis,target)
print ("Distribution before resampling {}".format(Counter(target)))
print ("Distribution labels after resampling {}".format(Counter(y_train_res)))
model_three = run_model_balanced(X_train_res,y_train_res)
y_pred_Over = model_three.predict(X_test_analysis)
y_prob_Over = model_three.predict_proba(X_test_analysis)
clientes_proba(y_pred_Over, y_prob_Over, "RandomOverSampler")
Mejora considerablemente. La naturaleza de los datos responde mejor a un aumento de la clase minoritaria que a una disminución de la clase mayortaria.
Esta técnica consiste en aplicar en simultáneo un algoritmo de subsampling y otro de oversampling a la vez al dataset. En este caso usaré SMOTE para oversampling: Se encarga de buscar puntos es decir vecinos cercanos y agrega puntos «en linea recta» entre ellos. Y usaré Tomek para undersampling que quita los de distinta clase que sean nearest neighbor y deja ver mejor el decisión boundary (la zona limítrofe de nuestras clases).
os_us = SMOTETomek(sampling_strategy='not minority')
X_train_res, y_train_res = os_us.fit_sample(X_train_analysis,target)
print ("Distribution before resampling {}".format(Counter(target)))
print ("Distribution after resampling {}".format(Counter(y_train_res)))
model = run_model_balanced(X_train_res,y_train_res)
y_pred_SMOTE = model.predict(X_test_analysis)
y_prob_SMOTE = model.predict_proba(X_test_analysis)
clientes_proba(y_pred_SMOTE , y_prob_SMOTE, "Smote-Tomek")
Para esta estrategia usaremos un Clasificador de Ensamble que utiliza Bagging y el modelo será un DecisionTree. Veamos como se comporta.
bbc = BalancedBaggingClassifier(base_estimator=DecisionTreeClassifier(),
sampling_strategy='auto',
replacement=False,
random_state=0)
#Train the classifier.
bbc.fit(X_train_analysis,target)
y_pred_bbc = bbc.predict(X_test_analysis)
y_prob_bbc = bbc.predict_proba(X_test_analysis)
clientes_proba(y_pred_bbc , y_prob_bbc, "BalancedBaggingClassifier")
A partir de aqui se mostrarán los resultados del modelo seleccionado como el más conveniente para predecir la fuga de clientes en una empresa de telecomunicaciones. Dado que son datos que se generarón de manera aletoria no es de esperar que los resultados obtenidos sean comparables a la realidad pero si los procedimientos de limpieza, análisis y mejora de modelos. Los principales retos consistierón en trabajar con un problema de desbalanceo de clases (existiendo una clase predominante y una minoritaria, fuga de clientes).Relación 92.6% y 7.4%. Problema de subespecificación del modelo : No poder contar con todas las variables y en cambio tener unas pocas que separaban de manera perfecta dicha relacion Permanencia/Abandono (Existencia de limitaciones y supuestos). En cuánto a las acciones comerciales podría existir una posibilidad de mejora, en el churn obtenido, aunque el índice de cancelación de los clientes puede ser asociado a distintos factores, es complicado decidir un porcentaje de churn ideal aunque lo esperado sea tener una rotación muy baja.
print("Probabilidad de Acierto: " +str(y_prob_test_logreg_1[0][y_pred_test_logreg_1]*100)+"%")
from id_clientes_prob import proba_ID
table_clientes =proba_ID(y_pred_test_logreg_1,y_prob_test_logreg_1,X_test_analysis,"LogisticRegression_1")
Está es la lista de 1565 clientes que el modelo clasificó como los más propensos a no permanecer en telefónica y la probabilidad de abandono que tiene cada cliente , esta lista es la que esperaría obtener la compañía para los respectivos procesos de evaluación una vez generado el modelo ganador. El modelo fue capaz de predecir el 1.69% de los clientes que no permanecerán el mes siguiente (Febrero).
pd.DataFrame(list(zip(X_train_analysis.columns, np.transpose(logreg_1.coef_))))
se puede ver como el incremento en segundos consumidos disminuyen la probabilidad de que el cliente se fugue.
print(X_train_analysis.iloc[85])
F=X_train_analysis.iloc[85]
F.shape
F.values.reshape(1,-1)
logreg_1.predict_proba(F.values.reshape(1,-1))
Para este cliente la probabilidad de abandono es de un 80%, Descripción : lo que incrementa la probabilidad de que este cliente se fugue son el tener entre 1 a 4 líneas en impago, No tener descuentos, No tener terminales financiados, el incremento en el consumo de llamadas entrantes así como en los mb de datos.
data_= y_pred_test_logreg_1
data_= pd.DataFrame(data_)
data_.columns = ["resul"]
data_["resul"].value_counts()
t = logreg_1.coef_
t = t.transpose()
importance_df = pd.DataFrame(t, columns=['Feature_Importance'],
index=X_train_analysis.columns)
importance_df.sort_values(by=['Feature_Importance'], ascending=True).plot(kind="barh",figsize=(12,10))
plt.xticks(rotation=80)
plt.show()
print(importance_df.sort_values(by=['Feature_Importance'], ascending=False))
En cuanto a las variables obtenidas posterior al proceso de selección de variables y la estimación del modelo de regresión logística, no es de sorprenderse que la permanencia de los clientes en la compañía dependerá en gran parte de los incentivos y el nivel de satisfacción, por lo que es imposible retener a un cliente insatisfecho , no obstante aún estando satisfechos con el servicio buscan activamente ofertas mejores o "descuentos" y podemos ver que está variable corresponde al top número 1 en el ranking esto es una clara alerta para la compañía una vez teniendo conocimiento de los clientes que están en riesgo de fuga podrá anticiparse a la hipercompetencia y conceder mejores ventajas a sus clientes. Por otro lado la segunda variable más importante Incidencia es claro que los clientes son muy suceptibles a tener una mala percepción del servicio con muy poco esfuerzo, por ejemplo un mal servicio por parte del proveedor , deficiencias en la calidad de los productos y porsupuesto un mal asesoramiento en atención al cliente: como por ejemplo líneas ocupadas el famoso permanezca en espera en breve le atenderemos, largos minutos de espera, errores en la facturación, entre otros.. la tercera variable más importante información sobre número de líneas en impago, no es nuevo que los clíentes de telefonía pueden cambiar libremente de compañía y conservando su número de teléfono aunque mantengan una deuda con la operadora, por lo que los clientes con número de líneas en impago son los más peligrosos ya que son los más inestables y por lo general se acostumbran a cambiar de una compañía a otra para eludir los pagos. Por ùltimo la cuarta variable más importante financiación es claro que los clientes que tengan fiananciado algún tipo de terminal están obligados a permanecer en la compañía contrarío a los que no, quienes tienen toda la libertad de marcharse cuando lo deseen (dependiendo el tipo de contrato y telefónia) podrían deberse a clientes nuevos con expectativas muy altas que sin se incumplen causan malestar y decepción, por ejemplo un grave error al captar clientes es prometer más de lo que se puede dar pero no siendo así como clientes somos un mundo muy incierto bajo constante cambio por lo que hay causas incontrolables en cuanto a la fuga de clientes que no tienen terminales financiados, como cambio de domicilio, fallecimiento del cliente,etc.
#X_train_analysis.to_csv('train.csv', header=True, index=True)
#X_test_analysis.to_csv('test.csv', header=True, index=True)
#target.to_csv('y.csv', header=True, index=True)